1. Introduction : importance des patrons

phdComics
Figure 1. Les patrons : des réponses éprouvées à des problèmes récurrents

Science is what we understand well enough to explain to a computer. Art is everything else we do.

— Donald Knuth

1.1. Strategy

1.1.1. Principes de conception

Principe de conception

Identifiez les aspects de votre code qui varient et séparez-les de ceux qui demeurent constant.

Principe de conception

Programmer une interface, non une implémentation.

Principe de conception

Préférez la composition à l’héritage.

1.1.2. Définition du patron

Design pattern : Stratégie (Strategy)

Stratégie définit une famille d’algorithmes, encapsule chacun d’eux et les rend interchangeables. Il permet à l’algorithme de varier indépendamment des clients qui l’utilisent.

strategy
Figure 2. Modèle UML du patron Strategy

1.1.3. Premier exemple d’utilisation

superCanardFinal a
Figure 3. Premier exemple d’utilisation de patron (1er comportement)
superCanardFinal b
Figure 4. Premier exemple d’utilisation de patron (2ème comportement)
superCanardFinal c
Figure 5. Premier exemple d’utilisation de patron (hiérarchie des classes)
Question
Pourquoi n’a-t’on pas utilisé Strategy pour afficher() ou nager()?

1.1.4. Autre exemple concret

L’exemple qui suit est tiré de ce cours.

Le problème

Vous avez une classe FileWriter qui a pour rôle d’écrire dans un fichier ainsi qu’une classe DBWriter. Dans un premier temps, ces classes ne contiennent qu’une méthode write() qui n’écrira que le texte passé en paramètre.

Au fil du temps, vous vous rendez compte que c’est dommage qu’elles ne fassent que ça et vous aimeriez bien qu’elles puissent écrire en différents formats (HTML, XML, etc.) : les classes doivent donc formater puis écrire.

La solution

strategyWriter
Figure 6. Application du pattern Strategy (source)
L’interface en PHP (code source ici)
<?php
interface Formater
{
  public function format($text);
}
?>
La classe abstraite Writer (code source ici)
<?php
abstract class Writer
{
  // Attribut contenant l'instance du formateur que l'on veut utiliser.
  protected $formater;

  abstract public function write($text);

  // Nous voulons une instance d'une classe implémentant Formater en paramètre.
  public function __construct(Formater $formater)
  {
    $this->formater = $formater;
  }
}
?>
La classe FileWriter (code source ici)
<?php
class FileWriter extends Writer
{
  // Attribut stockant le chemin du fichier.
  protected $file;

  public function __construct(Formater $formater, $file)
  {
    parent::__construct($formater);
    $this->file = $file;
  }

  public function write($text)
  {
    $f = fopen($this->file, 'w');
    fwrite($f, $this->formater->format($text));
    fclose($f);
  }
}
?>
La classe DBWriter (code source ici)
<?php
class DBWriter extends Writer
{
  protected $db;

  public function __construct(Formater $formater, PDO $db)
  {
    parent::__construct($formater);
    $this->db = $db;
  }

  public function write ($text)
  {
    $q = $this->db->prepare('INSERT INTO lorem_ipsum SET text = :text');
    $q->bindValue(':text', $this->formater->format($text));
    $q->execute();
  }
}
?>

Enfin, nous avons nos trois formateurs. L’un ne fait rien de particulier (TextFormater), et les deux autres formatent le texte en deux langages différents (HTMLFormater et XMLFormater).

La classe TextFormater (code source ici)
<?php
class TextFormater implements Formater
{
  public function format($text)
  {
    return 'Date : ' . time() . "\n" . 'Texte : ' . $text;
  }
}
?>
La classe HTMLFormater (code source ici)
<?php
class HTMLFormater implements Formater
{
  public function format($text)
  {
    return '<p>Date : ' . time() . '<br />' ."\n". 'Texte : ' . $text . '</p>';
  }
}
?>
La classe XMLFormater (code source ici)
<?php
class XMLFormater implements Formater
{
  public function format($text)
  {
    return '<?xml version="1.0" encoding="ISO-8859-1"?>' ."\n".
           '<message>' ."\n".
           "\t". '<date>' . time() . '</date>' ."\n".
           "\t". '<texte>' . $text . '</texte>' ."\n".
           '</message>';
  }
}
?>

1.1.5. D’autres exemples

  • La fonction standard sort() de python

    >>> sorted("This is a test string from Andrew".split(), key=str.lower)
    ['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
  • Stratégie de cryptage en fonction de la taille d’un fichier

    File file = getFile();
     Cipher c = CipherFactory.getCipher( file.size() );
     c.performAction();
    
    // implementations:
    interface  Cipher  {
         public void performAction();
    }
    class InMemoryCipherStrategy implements Cipher {
             public void performAction() {
                 // load in byte[] ....
             }
    }
    class SwaptToDiskCipher implements Cipher {
             public void performAction() {
                 // swapt partial results to file.
             }
    }
Plus de détails ici

1.2. (non) Réutilisation

Les patrons ne sont pas réutilisables!

Il faut implémeter la solution qu’il représente à chaque fois.

Exception : certains font l’objet d’une librairie (comme Observer de Java).

Par exemple le patron Singleton existe dans la bibliothèque standard du langage en Ruby. C’est un mixin qu’il suffit d’inclure dans la classe qui doit être un singleton.

class Klass
   include Singleton
   # ...
end

a,b  = Klass.instance, Klass.instance

a == b
# => true

Klass.new
# => NoMethodError - new is private ...

1.3. Association ou composition

On trouve deux modèles UML™ :

strategy compo
Figure 7. Strategy et composition
strategy assoc
Figure 8. Strategy et association

Et donc deux implémentations :

Composition ⇒ le composé encapsule les composants
public class Colvert extends Canard {

	protected Colvert() {
		this(new VolerAvecDesAiles(), new Cancan());
	}
...
c1 = new Colvert();
Association ⇒ le composant existe "en dehors"
...
vol = new VolerAvecDesAiles();
cri = new Cancan();
c1 = new Colvert(vol,cri);
...

2. Un peu d’histoire

1977

Alexander : patterns pour les architectures (les vraies)

Alexander
1987

Beck et Cunningham : patterns pour des interfaces utilisateurs

1988

Meyer : livre sur l’orienté objet (langage Eiffel), devenu la bible pour beaucoup de programmeurs (cf. [Meyer88])

1990-1995

Gamma, Helm, Johnson et Vlissides : LE livre de référence (cf. [GoF])

GoF
Les auteurs de ce livre sont connus comme les Gof pour « Gang of Four ».
2003

Martin : principes SOLID (cf. [Martin03])

2004

Craig Larman décrit des modèles de conception : les Patterns GRASP (cf. [Larman05])

3. Exemples de bons principes

SOLID:

  • Single Responsibility Principle

  • Open-Closed Principle

  • Liskov Substitution Principle

  • Interface Segregation Principle

  • Dependency Inversion Principle

3.1. Single Responsibility Principle

solid s
Figure 9. Single Responsibility Principle (source [SOLID])
Responsabilité => Sujet à changement

3.2. Open-Closed Principle

solid o
Figure 10. Open-Closed Principle (source [SOLID])
Ouvert à l'extension mais fermé à la modification

Ainsi, une fois écrite et testée, une classe ne devrait être modifiée que pour être corrigée! Toute modification devrait être possible par extension.

3.3. Liskov Substitution Principle

Barbara Liskov, pionnière en informatique et plus précisément en OO, a donné son nom à un principe important et bien connu : le principe default: substitution de Liskov. Elle a reçu l’équivalent du prix Nobel d’Informatique (le Turing Award) en 2009.

liskov
Figure 11. Barbara Liskov reçoit le Turing Award
liskov2018
Figure 12. Barbara Liskov est toujours active!
solid l
Une classe doit pouvoir être remplacée par une instance d'un de ses
sous-types, sans modifier la cohérence du programme

Un carré est un rectangle particulier.

carre
Figure 13. Exemple classique de violation du principe de substitution de Liskov
Question
Peut-on toujours substituer un Carré à la place d’un Rectangle ?
Réponse (Rectangle.java)
class Rectangle
{
	protected int m_width;
	protected int m_height;

	public void setWidth(int width){
		m_width = width;
	}

	public void setHeight(int height){
		m_height = height;
	}


	public int getWidth(){
		return m_width;
	}

	public int getHeight(){
		return m_height;
	}

	public int getArea(){
		return m_width * m_height;
	}
}
Réponse (Square.java)
// Violation of Likov's Substitution Principle
class Square extends Rectangle
{
	public void setWidth(int width){
		m_width = width;
		m_height = width;
	}

	public void setHeight(int height){
		m_width = height;
		m_height = height;
	}

}
Réponse (Square.java - suite)
class LspTest
{
	private static Rectangle getNewRectangle()
	{
		// it can be an object returned by some factory ...
		return new Square();
	}

	public static void main (String args[])
	{
		Rectangle r = LspTest.getNewRectangle();

		r.setWidth(5);
		r.setHeight(10);
		// User knows that r is a rectangle.
		// It assumes that he's able to set the width and height as for the base class

		System.out.println(r.getArea());
		// Now she's surprised to see that the area is 100 instead of 50.
	}
}
rectangle
Figure 14. Et si on essaye l’inverse
Réponse (Rectangle.java)
class LspTest
{
	private static Square getNewSquare()
	{
		// it can be an object returned by some factory ...
		return new Rectangle();
	}

	public static void main (String args[])
	{
		Square s = LspTest.getNewSquare();

		s.setWidth(5);
		// User knows that r is a rectangle.
		// It assumes that he's able to set the width and height as for the base class

		System.out.println(s.getArea());
		// Now she's surprised to see that the area is 0 instead of 25.
	}
}

3.4. Interface Segregation Principle

solid i
Préférer plusieurs interfaces spécifiques pour chaque client plutôt qu'une seule interface générale

3.5. Dependency Inversion Principle

solid d
Il faut dépendre des abstractions, pas des implémentations

Ce principe indique :

  • Les modules de haut niveau (abstraits) ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.

  • Les abstractions ne doivent pas dépendre des détails d’implémentation. C’est l’inverse : les détails doivent dépendre des abstractions.

Ainsi ce principe va à l’encontre de l’intuition classique.
dip bad
Figure 15. Exemple de code violant le principe d’inversion des dépendances
dip
Figure 16. Exemple de code ne violant plus le principe d’inversion des dépendances

3.6. SOLID et patrons

QUESTION

Lesquels des 5 principes SOLID s’appliquent bien à Strategy ?

3.7. GRASP

The critical design tool for software development is a mind well educated in design principles. It is not the UML or any other technology.

— Craig Larman
2005

Il s’agit d’un ensemble de patrons, plutôt orientés conception (UML). Nous en aborderons certains au travers des exemples de ce module (cf. [Larman05]).

Notez que les principes SOLID ne s’appliquent pas qu’à la programmation objet. Pour une discussion sur leur application avec React (language fonctionnel), cf. https://dev.to/shadid12/can-you-apply-solid-principles-to-your-react-applications-46il.

4. Les patrons : comment ça marche ?

4.1. Intérêt

  • Réponses éprouvées à des problèmes récurrents

  • Vocabulaire commun

T’as qu’à utiliser une factory!

4.3. Patrons non abordés

  • Décorateur

  • Commande

  • Façade

  • Patron de méthode

  • Chaînes de responsabilité

  • Prototype

  • Mémento

  • Médiateur

  • Interprète

  • Poids-mouche

  • Monteur

  • Pont